Handling User Input — SwiftUI Tutorials | Apple Developer Documentation
在Landmarks应用程序中,用户可以标记他们最喜欢的地方,并过滤列表,只显示他们最喜欢的地方。要实现该功能,首先要在列表中添加一个开关,这样用户就可以只关注自己的最爱,然后再添加一个星形按钮,用户点击该按钮就可以将某个地标标记为最爱。
标记用户最喜欢的landmark
struct LandmarkRow: View {
var landmark: Landmark
var body: some View {
HStack {
landmark.image.resizable().frame(width: 50,height: 50)
Text(landmark.name)
Spacer()
// 增加判断
if landmark.isFavorite {
Image(systemName: "star.fill")
.foregroundColor(.yellow)
}
}
}
}
struct LandmarkRow: View {
var landmark: Landmark
var body: some View {
HStack {
landmark.image.resizable().frame(width: 50,height: 50)
Text(landmark.name)
Spacer()
// 增加判断
if landmark.isFavorite {
Image(systemName: "star.fill")
.foregroundColor(.yellow)
}
}
}
}
过滤用户最喜欢的landmark
- 增加状态标记是否需要进行过滤
import SwiftUI
struct LandmarkList: View {
// 由于您使用状态属性来保存特定于视图及其子视图的信息,因此以 private 的形式创建状态。
@State
private var showFavoriteOnly = false
var filteredLandmarks:[Landmark] {
landmarks.filter { item in
return showFavoriteOnly ? item.isFavorite : true
}
}
var body: some View {
NavigationView {
List(filteredLandmarks) { item in
NavigationLink {
LandmarkDetail(landmark: item)
} label: {
LandmarkRow(landmark: item)
}
}.navigationTitle("landmark")
}
}
}
import SwiftUI
struct LandmarkList: View {
// 由于您使用状态属性来保存特定于视图及其子视图的信息,因此以 private 的形式创建状态。
@State
private var showFavoriteOnly = false
var filteredLandmarks:[Landmark] {
landmarks.filter { item in
return showFavoriteOnly ? item.isFavorite : true
}
}
var body: some View {
NavigationView {
List(filteredLandmarks) { item in
NavigationLink {
LandmarkDetail(landmark: item)
} label: {
LandmarkRow(landmark: item)
}
}.navigationTitle("landmark")
}
}
}
2). 增加toggle控件,控制当前是否需要过滤。为了直接把Toggle放到list中去,改用ForEach进行遍历显示组件。
import SwiftUI
struct LandmarkList: View {
// 由于您使用状态属性来保存特定于视图及其子视图的信息,因此以 private 的形式创建状态。
@State
private var showFavoriteOnly = false
var filteredLandmarks:[Landmark] {
landmarks.filter { item in
return showFavoriteOnly ? item.isFavorite : true
}
}
var body: some View {
NavigationView {
// List(landmarks) { item in
// NavigationLink {
// LandmarkDetail(landmark: item)
// } label: {
// LandmarkRow(landmark: item)
// }
// }.navigationTitle("landmark")
List{
Toggle(isOn: $showFavoriteOnly) {
Text("Favorites only")
}
ForEach(filteredLandmarks) { landmark in
NavigationLink {
LandmarkDetail(landmark: landmark)
} label: {
LandmarkRow(landmark: landmark)
}
}
}.navigationTitle("landmark")
}
}
}
import SwiftUI
struct LandmarkList: View {
// 由于您使用状态属性来保存特定于视图及其子视图的信息,因此以 private 的形式创建状态。
@State
private var showFavoriteOnly = false
var filteredLandmarks:[Landmark] {
landmarks.filter { item in
return showFavoriteOnly ? item.isFavorite : true
}
}
var body: some View {
NavigationView {
// List(landmarks) { item in
// NavigationLink {
// LandmarkDetail(landmark: item)
// } label: {
// LandmarkRow(landmark: item)
// }
// }.navigationTitle("landmark")
List{
Toggle(isOn: $showFavoriteOnly) {
Text("Favorites only")
}
ForEach(filteredLandmarks) { landmark in
NavigationLink {
LandmarkDetail(landmark: landmark)
} label: {
LandmarkRow(landmark: landmark)
}
}
}.navigationTitle("landmark")
}
}
}
使用Observable对象进行存储
为了让用户把自己喜欢的地标放入收藏夹,首先需要将地标landmark数据放入到可观察(Observable)对象中。
可观察对象是您的数据的自定义对象,可从SwiftUI环境中的存储绑定到视图。SwiftUI会监视对可观察对象的任何可能影响视图的更改,并在更改后显示正确版本的视图。
1). 修改数据源
import Combine
import Foundation
// var landmarks: [Landmark] = load("landmarkData.json")
// SwiftUI订阅(subscribes)您的可观察(observable)对象,并在数据变化时更新任何需要刷新的视图。
final class ModelData: ObservableObject {
// 可观察对象需要发布对其数据的任何更改,以便其订阅者能够接收更改。
@Published var landmarks:[Landmark] = load("landmarkData.json")
}
func load<T: Decodable>(_ filename: String) -> T {
let data: Data
guard let file = Bundle.main.url(forResource: filename, withExtension: nil)
else {
fatalError("Couldn't find \(filename) in main bundle.")
}
do {
data = try Data(contentsOf: file)
} catch {
fatalError("Couldn't load \(filename) from main bundle:\n\(error)")
}
do {
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: data)
} catch {
fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)")
}
}
import Combine
import Foundation
// var landmarks: [Landmark] = load("landmarkData.json")
// SwiftUI订阅(subscribes)您的可观察(observable)对象,并在数据变化时更新任何需要刷新的视图。
final class ModelData: ObservableObject {
// 可观察对象需要发布对其数据的任何更改,以便其订阅者能够接收更改。
@Published var landmarks:[Landmark] = load("landmarkData.json")
}
func load<T: Decodable>(_ filename: String) -> T {
let data: Data
guard let file = Bundle.main.url(forResource: filename, withExtension: nil)
else {
fatalError("Couldn't find \(filename) in main bundle.")
}
do {
data = try Data(contentsOf: file)
} catch {
fatalError("Couldn't load \(filename) from main bundle:\n\(error)")
}
do {
let decoder = JSONDecoder()
return try decoder.decode(T.self, from: data)
} catch {
fatalError("Couldn't parse \(filename) as \(T.self):\n\(error)")
}
}
2). 页面使用模型对象:LandmarkList页面
@ EnvironmentObject是一种属性包装器,用于将环境对象注入到视图层次结构中的任何地方,并使其可用于所有层次结构中的子视图。环境对象是一个可观察的对象,可以包含任意类型的数据,并且可以在整个应用程序中共享和使用,例如应用程序的设置或用户身份验证。@EnvironmentObject属性包装器的使用涉及以下步骤:
- 创建一个ObservableObject的自定义类并放入一些需要跨视图共享的属性。
- 在应用程序的顶层视图中使用@EnvironmentObject注册此自定义类。
- 在需要访问环境对象的任何视图中使用@EnvironmentObject属性包装器获取访问这个环境对象的能力
import SwiftUI
struct LandmarkList: View {
@EnvironmentObject var modelData: ModelData
// 由于您使用状态属性来保存特定于视图及其子视图的信息,因此以 private 的形式创建状态。
@State
private var showFavoriteOnly = false
var filteredLandmarks:[Landmark] {
// landmarks.filter { item in
modelData.landmarks.filter { item in
return showFavoriteOnly ? item.isFavorite : true
}
}
var body: some View {
NavigationView {
List{
Toggle(isOn: $showFavoriteOnly) {
Text("Favorites only")
}
ForEach(filteredLandmarks) { landmark in
NavigationLink {
LandmarkDetail(landmark: landmark)
} label: {
LandmarkRow(landmark: landmark)
}
}
}.navigationTitle("landmark")
}
}
}
struct LandmarkList_Previews: PreviewProvider {
static var previews: some View {
// LandmarkList()
// 使用的是 .environmentObject 初始化
LandmarkList().environmentObject(ModelData())
}
}
import SwiftUI
struct LandmarkList: View {
@EnvironmentObject var modelData: ModelData
// 由于您使用状态属性来保存特定于视图及其子视图的信息,因此以 private 的形式创建状态。
@State
private var showFavoriteOnly = false
var filteredLandmarks:[Landmark] {
// landmarks.filter { item in
modelData.landmarks.filter { item in
return showFavoriteOnly ? item.isFavorite : true
}
}
var body: some View {
NavigationView {
List{
Toggle(isOn: $showFavoriteOnly) {
Text("Favorites only")
}
ForEach(filteredLandmarks) { landmark in
NavigationLink {
LandmarkDetail(landmark: landmark)
} label: {
LandmarkRow(landmark: landmark)
}
}
}.navigationTitle("landmark")
}
}
}
struct LandmarkList_Previews: PreviewProvider {
static var previews: some View {
// LandmarkList()
// 使用的是 .environmentObject 初始化
LandmarkList().environmentObject(ModelData())
}
}
修改contentView,这里用到了@StateObject
:
使用
@StateObject
属性只能在应用程序的生命周期中为给定属性初始化模型对象一次。在应用程序实例中使用该属性(如图所示)以及在视图中使用该属性时都是如此。
相关名词解释(来自chatgpt):
@StateObject 是一个属性包装器,用于将对象作为 State 在视图中进行管理。SwiftUI中,每当属性的值更改时,所有依赖该值的视图都会重新计算和更新,@StateObject 通过将其包装的对象作为State来管理其状态,并确保只创建一个实例。当对象的状态改变时,SwiftUI会自动重新计算视图。
使用 @StateObject 的好处是可以将对象的生命周期委托给 SwiftUI 管理,也可以方便地在不同的视图之间共享相同的实例。这对于需要使用长时间存在的对象(例如网络请求、数据库连接或其他长时间运行的任务)的视图非常有用。
struct ContentView: View {
@StateObject private var modelData = ModelData()
var body: some View {
// LandmarkList()
LandmarkList().environmentObject(modelData)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
struct ContentView: View {
@StateObject private var modelData = ModelData()
var body: some View {
// LandmarkList()
LandmarkList().environmentObject(modelData)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
通过创建“收藏按钮”进行数据交互
1). 创建收藏按钮
import SwiftUI
struct FavoriteButton: View {
@Binding var isSet: Bool
var body: some View {
Button {
// 使用该方法将布尔值从true切换为false,或从false切换为true。
isSet.toggle()
} label: {
Label("Toggle Favorite", systemImage: isSet ? "star.fill" : "star")
.labelStyle(.iconOnly)
.foregroundColor(isSet ? .yellow : .gray)
}
}
}
struct FavoriteButton_Previews: PreviewProvider {
static var previews: some View {
FavoriteButton(isSet: .constant(true))
}
}
import SwiftUI
struct FavoriteButton: View {
@Binding var isSet: Bool
var body: some View {
Button {
// 使用该方法将布尔值从true切换为false,或从false切换为true。
isSet.toggle()
} label: {
Label("Toggle Favorite", systemImage: isSet ? "star.fill" : "star")
.labelStyle(.iconOnly)
.foregroundColor(isSet ? .yellow : .gray)
}
}
}
struct FavoriteButton_Previews: PreviewProvider {
static var previews: some View {
FavoriteButton(isSet: .constant(true))
}
}
2). 增加交互
import SwiftUI
struct LandmarkDetail: View {
var landmark:Landmark
@EnvironmentObject var modelData:ModelData
var landmarkIndex: Int {
modelData.landmarks.firstIndex(where: { $0.id == landmark.id })!
}
var body: some View {
ScrollView {
MapView(coordinates: landmark.locationCoordinate)
.ignoresSafeArea()
.frame(height: 300)
CircleImage(image: landmark.image)
.offset(y: -180)
.padding(.bottom, -180)
VStack(alignment: .leading) {
HStack {
Text(landmark.name)
.font(.title)
.foregroundColor(.cyan)
FavoriteButton(isSet: $modelData.landmarks[landmarkIndex].isFavorite)
}
HStack {
Text(landmark.park)
.font(.subheadline)
Spacer()
Text(landmark.state)
.font(.subheadline)
}
Divider()
Text("About \(landmark.name)").font(.title2)
Text(landmark.description).font(.subheadline)
}
}.navigationTitle(landmark.name)
.navigationBarTitleDisplayMode(.inline)
}
}
struct LandmarkDetail_Previews: PreviewProvider {
static let modelData = ModelData()
static var previews: some View {
LandmarkDetail(landmark: ModelData().landmarks[1])
.environmentObject(modelData)
}
}
import SwiftUI
struct LandmarkDetail: View {
var landmark:Landmark
@EnvironmentObject var modelData:ModelData
var landmarkIndex: Int {
modelData.landmarks.firstIndex(where: { $0.id == landmark.id })!
}
var body: some View {
ScrollView {
MapView(coordinates: landmark.locationCoordinate)
.ignoresSafeArea()
.frame(height: 300)
CircleImage(image: landmark.image)
.offset(y: -180)
.padding(.bottom, -180)
VStack(alignment: .leading) {
HStack {
Text(landmark.name)
.font(.title)
.foregroundColor(.cyan)
FavoriteButton(isSet: $modelData.landmarks[landmarkIndex].isFavorite)
}
HStack {
Text(landmark.park)
.font(.subheadline)
Spacer()
Text(landmark.state)
.font(.subheadline)
}
Divider()
Text("About \(landmark.name)").font(.title2)
Text(landmark.description).font(.subheadline)
}
}.navigationTitle(landmark.name)
.navigationBarTitleDisplayMode(.inline)
}
}
struct LandmarkDetail_Previews: PreviewProvider {
static let modelData = ModelData()
static var previews: some View {
LandmarkDetail(landmark: ModelData().landmarks[1])
.environmentObject(modelData)
}
}